/*
 * SmartGWT Extensions Copyright (c) 2009, Hang (Anthony) Yuan.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:

 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.

 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

package com.binhthuan.catechist.client.component.fileuploader;

import java.util.ArrayList;
import java.util.Set;

import com.binhthuan.catechist.client.component.fileuploader.event.BeforeFileAddEvent;
import com.binhthuan.catechist.client.component.fileuploader.event.BeforeFileAddHandler;
import com.binhthuan.catechist.client.component.fileuploader.event.FileAddEvent;
import com.binhthuan.catechist.client.component.fileuploader.event.FileAddHandler;
import com.binhthuan.catechist.client.component.fileuploader.event.FileQueueResetEvent;
import com.binhthuan.catechist.client.component.fileuploader.event.FileQueueResetHandler;
import com.binhthuan.catechist.client.component.fileuploader.event.FileRemoveEvent;
import com.binhthuan.catechist.client.component.fileuploader.event.FileRemoveHandler;
import com.binhthuan.catechist.client.component.fileuploader.event.FileSelectedEvent;
import com.binhthuan.catechist.client.component.fileuploader.event.FileSelectedHandler;
import com.binhthuan.catechist.client.component.fileuploader.event.FileUploadFailedEvent;
import com.binhthuan.catechist.client.component.fileuploader.event.FileUploadFailedHandler;
import com.binhthuan.catechist.client.component.fileuploader.event.FileUploadStartEvent;
import com.binhthuan.catechist.client.component.fileuploader.event.FileUploadStartHandler;
import com.binhthuan.catechist.client.component.fileuploader.event.FileUploadSuccessEvent;
import com.binhthuan.catechist.client.component.fileuploader.event.FileUploadSuccessHandler;
import com.binhthuan.catechist.client.component.fileuploader.event.UploadCompletedEvent;
import com.binhthuan.catechist.client.component.fileuploader.event.UploadCompletedHandler;
import com.binhthuan.catechist.client.component.fileuploader.event.UploadStartEvent;
import com.binhthuan.catechist.client.component.fileuploader.event.UploadStartHandler;
import com.binhthuan.catechist.client.component.fileuploader.event.UploadStopEvent;
import com.binhthuan.catechist.client.component.fileuploader.event.UploadStopHandler;
import com.google.gwt.core.client.GWT;
import com.google.gwt.event.shared.HandlerRegistration;
import com.google.gwt.i18n.client.Dictionary;
import com.google.gwt.json.client.JSONObject;
import com.google.gwt.json.client.JSONParser;
import com.google.gwt.json.client.JSONValue;
import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.Element;
import com.google.gwt.user.client.ui.FlowPanel;
import com.google.gwt.user.client.ui.FormPanel.SubmitCompleteEvent;
import com.google.gwt.user.client.ui.FormPanel.SubmitCompleteHandler;
import com.google.gwt.user.client.ui.FormPanel.SubmitEvent;
import com.google.gwt.user.client.ui.FormPanel.SubmitHandler;
import com.smartgwt.client.widgets.Button;
import com.smartgwt.client.widgets.Canvas;
import com.smartgwt.client.widgets.Progressbar;
import com.smartgwt.client.widgets.WidgetCanvas;
import com.smartgwt.client.widgets.Window;
import com.smartgwt.client.widgets.events.ClickEvent;
import com.smartgwt.client.widgets.events.ClickHandler;
import com.smartgwt.client.widgets.events.DrawEvent;
import com.smartgwt.client.widgets.events.DrawHandler;
import com.smartgwt.client.widgets.grid.ListGrid;
import com.smartgwt.client.widgets.toolbar.ToolStrip;

/**
 * A window that allow user to upload files to remote server.
 * 
 * @author anthony.yuan@gmail.com
 */
public class FileUploader extends Window implements FileUploaderConstants {

    private static int NEXT_ID = 1;

    private String url;
    private String postVarName = "file";
    private Dictionary messageDictionary;

    private ArrayList<FileNameFilter> fileFilters;

    private FileUploadInput lastNewFileInput;
    private Progressbar progressbar;
    private FileGrid fileGrid;
    private Button addFileDummyButton;
    private Button uploadFileButton;
    private Button stopFileButton;
    private Button resetFileButton;
    private FlowPanel newFileInputContainer;
    private FileUploadingForm formFileUpload;
    private int uploadFileNum = 0;
    private FileRecord uploadingFileRecord;

    /* - Utility functions -------------------------------------------------- */
    /**
     * Create a FileUploader and show it.
     * 
     * @param title
     *            title of the dialog
     * @param uiLang
     *            language code for UI and message. e.g. 'en', 'fr', etc.
     * @param afterUploadedCallback
     * @return generated FileUploader
     */
    public static FileUploader popup(final String title, final String uiLang) {
        FileUploader dialogBox = new FileUploader(title, uiLang);
        dialogBox.setIsModal(true);
        dialogBox.setShowModalMask(true);
        dialogBox.setCanDragResize(true);

        dialogBox.setAutoCenter(true);
        dialogBox.show();

        return dialogBox;
    }

    /**
     * Create a new Window.
     * 
     * @param title
     *            the title
     * @param uiLang
     *            language code for UI and message
     */
    public FileUploader(String title, String uiLang) {
        assert uiLang != null;
        setTitle(title);
        setAutoSize(true);
        setCanDragResize(true);

        this.messageDictionary = FileUploaderUtils.getDictionary(uiLang);

        create(MIN_WIDTH, MIN_HEIGHT);
    }

    /* - Getters and Setters ------------------------------------------------ */
    /**
     * returns the URL where the files are posted
     * 
     * @return the String URL where files are posted
     */
    public String getUrl() {
        return url;
    }

    /**
     * sets the URL to post the files
     * 
     * @param url
     *            the String URL where files are posted
     */
    public void setUrl(String url) {
        this.url = url;
    }

    /**
     * sets the name of the variable to use for the files uploaded to the
     * server. By default it is "file"
     * 
     * @param varname
     *            name of the variable to use for the files uploaded to the
     *            server. By default it is "file"
     */
    public void setPostVarName(String postVarName) {
        this.postVarName = postVarName;
    }

    /* - Listener Handling -------------------------------------------------- */
    public HandlerRegistration addBeforeFileAddHandler(BeforeFileAddHandler handler) {
        return doAddHandler(handler, BeforeFileAddEvent.getType());
    }

    public HandlerRegistration addFileAddHandler(FileAddHandler handler) {
        return doAddHandler(handler, FileAddEvent.getType());
    }

    public HandlerRegistration addFileQueueResetHandler(FileQueueResetHandler handler) {
        return doAddHandler(handler, FileQueueResetEvent.getType());
    }

    public HandlerRegistration addFileRemoveResetHandler(FileRemoveHandler handler) {
        return doAddHandler(handler, FileRemoveEvent.getType());
    }

    public HandlerRegistration addFileUploadFailedResetHandler(FileUploadFailedHandler handler) {
        return doAddHandler(handler, FileUploadFailedEvent.getType());
    }

    public HandlerRegistration addFileUploadStartResetHandler(FileUploadStartHandler handler) {
        return doAddHandler(handler, FileUploadStartEvent.getType());
    }

    public HandlerRegistration addFileUploadSuccessResetHandler(FileUploadSuccessHandler handler) {
        return doAddHandler(handler, FileUploadSuccessEvent.getType());
    }

    public HandlerRegistration addUploadCompletedHandler(UploadCompletedHandler handler) {
        return doAddHandler(handler, UploadCompletedEvent.getType());
    }

    public HandlerRegistration addUploadStartHandler(UploadStartHandler handler) {
        return doAddHandler(handler, UploadStartEvent.getType());
    }

    public HandlerRegistration addUploadStopHandler(UploadStopHandler handler) {
        return doAddHandler(handler, UploadStopEvent.getType());
    }

    /**
     * Add FileNameFilter.
     * 
     * @param filter
     *            the FileNameFilter implementation
     */
    public void addFileNameFilter(FileNameFilter filter) {
        if (fileFilters == null) {
            fileFilters = new ArrayList<FileNameFilter>();
        }
        fileFilters.add(filter);
    }

    /**
     * Removes a FileNameFilter.
     * 
     * @param filter
     *            the FileNameFilter implementation to be removed
     */
    public void removeFileNameFilter(FileNameFilter filter) {
        if (fileFilters != null && fileFilters.contains(filter)) {
            fileFilters.remove(filter);
        }
    }

    /* - Create controls and layout ----------------------------------------- */
    private void create(final int defaultWidth, final int defaultHeight) {
        setMinHeight(MIN_HEIGHT);
        setMinWidth(MIN_WIDTH);
        progressbar = new Progressbar() {
            {
                setVertical(false);
                setHeight(DEFAULT_HEIGHT_PROCESSING_BAR);
                setLength(defaultWidth);
                setDefaultWidth(defaultWidth);
            }
        };
        addItem(progressbar);

        addItem(createFileGrid(defaultWidth, defaultHeight));

        ToolStrip toolStrip = new ToolStrip() {
            {
                setDefaultWidth(MIN_WIDTH);
                setAutoWidth();
            }
        };

        newFileInputContainer = new FlowPanel();
        newFileInputContainer.addStyleName(CSS_NEW_FILE_INPUT_CONTAINER);
        toolStrip.addMember(newFileInputContainer);

        addFileDummyButton = new SimpleButton(messageDictionary.get(LABEL_ADD), ICON_ADD);
        newFileInputContainer.add(addFileDummyButton);

        final Button removeFileButton = new SimpleButton(messageDictionary.get(LABEL_REMOVE), ICON_REMOVE);
        removeFileButton.addClickHandler(new ClickHandler() {
            public void onClick(ClickEvent event) {
                removeButtonOnClick();
            }
        });
        toolStrip.addMember(removeFileButton);

        resetFileButton = new SimpleButton(messageDictionary.get(LABEL_RESET), ICON_RESET);
        resetFileButton.addClickHandler(new ClickHandler() {
            public void onClick(ClickEvent event) {
                resetButtonOnClick();
            }
        });

        toolStrip.addMember(resetFileButton);

        uploadFileButton = new SimpleButton(messageDictionary.get(LABEL_UPLOAD), ICON_START);
        uploadFileButton.addClickHandler(new ClickHandler() {
            public void onClick(ClickEvent event) {
                uploadButtonOnClick();
            }
        });
        toolStrip.addMember(uploadFileButton);

        stopFileButton = new SimpleButton(messageDictionary.get(LABEL_STOP), ICON_STOP);
        stopFileButton.addClickHandler(new ClickHandler() {
            public void onClick(ClickEvent event) {
                stopButtonOnClick();
            }
        });
        toolStrip.addMember(stopFileButton);

        addItem(toolStrip);

        addDrawHandler(new DrawHandler() {
            public void onDraw(DrawEvent event) {
                addNewFileInput();
                resizeLastNewFileInput();
            }
        });
    }

    private void resizeLastNewFileInput() {
        int x = addFileDummyButton.getLeft();
        int y = addFileDummyButton.getTop();
        // int z = addFileButton.getZIndex();

        Element lastFileInputElement = lastNewFileInput.getElement();

        DOM.setStyleAttribute(lastFileInputElement, "left", "" + (x - 50));
        DOM.setStyleAttribute(lastFileInputElement, "top", "" + (y - 2));
        // DOM.setStyleAttribute(lastFileInputElement, "z-index", "" + (z + 1));
        addFileDummyButton.setZIndex(0);
    }

    private ListGrid createFileGrid(int defaultWidth, int defaultHeight) {
        fileGrid = new FileGrid(messageDictionary);
        fileGrid.setDefaultWidth(defaultWidth);
        fileGrid.setDefaultHeight(defaultHeight - DEFAULT_HEIGHT_PROCESSING_BAR - DEFAULT_HEIGHT_TOOL_STRIP);

        FileDS fileDS = new FileDS(messageDictionary);
        fileDS.setClientOnly(true);

        fileGrid.setDataSource(fileDS);
        fileGrid.setAutoFetchData(true);

        return fileGrid;
    }

    private void addNewFileInput() {
        final FileUploadInput newFileInput = new FileUploadInput(postVarName, fileFilters, messageDictionary);
        newFileInput.addFileSelectedHandler(new FileSelectedHandler() {
            public void onSelect(FileSelectedEvent event) {
                newFileInputOnSelect(event);
            }
        });
        lastNewFileInput = newFileInput;
        newFileInputContainer.add(lastNewFileInput);
    }

    private void newFileInputOnSelect(FileSelectedEvent event) {
        String fileName = event.getFilename();

        // create FileRecord
        FileRecord fileRecord = new FileRecord();
        fileRecord.setFileId(generateID());
        fileRecord.setFileState(FileState.STATE_QUEUE);
        fileRecord.setFileName(fileName);
        fileRecord.setNote(messageDictionary.get(NOTE_QUEUED));

        // fire BeforeFileAddEvent
        BeforeFileAddEvent newEvent = new BeforeFileAddEvent(FileUploader.this, fileName);
        fireEvent(newEvent);
        if (newEvent.isCancelled()) {
            return;
        }

        // move lastFileInput to fileUploadMap
        FileUploadInput fileUploadInput = event.getFileUploadInput();
        fileUploadInput.setVisible(false);
        newFileInputContainer.remove(fileUploadInput);

        // show this file in file grid
        fileGrid.addData(fileRecord);

        // create a new FileUpload
        addNewFileInput();
        resizeLastNewFileInput();

        // fire FileAddEvent
        fireEvent(new FileAddEvent(FileUploader.this, fileName));

    }

    /* - Control Event handling --------------------------------------------- */
    /**
     * Invoke to start uploading files.
     */
    private void uploadButtonOnClick() {
        // 1. create a file list for uploading
        if (fileGrid.getTotalRows() == 0) {
            return;
        }
        // 2. count uploading files
        uploadFileNum = 0;
        for (FileRecord fileRecord : fileGrid.getFileRecords()) {
            if (FileState.STATE_QUEUE.equals(fileRecord.getFileState())) {
                uploadFileNum++;
            }
        }
        if (uploadFileNum == 0) {
            return;
        }

        // 3. disable the uploadFileButton first
        uploadFileButton.disable();
        resetFileButton.disable();

        // 4. create a iframe for uploading files
        if (formFileUpload == null) {
            createUploadingForm();
        }

        // 5. initialize progressbar
        progressbar.setPercentDone(0);

        // 6. invoke listeners
        fireEvent(new UploadStartEvent(FileUploader.this));

        // 7. start uploading
        processNextFile();
    }

    /**
     * Invoke to reset the file list.
     */
    private void resetButtonOnClick() {
        fileGrid.setRecords(new FileRecord[0]);
        progressbar.setPercentDone(0);

        fireEvent(new FileQueueResetEvent(this));
    }

    /**
     * Invoke to selected file from list.
     */
    private void removeButtonOnClick() {
        if (!fileGrid.anySelected()) {
            return;
        }

        for (FileRecord fileRecord : fileGrid.getSelection()) {
            if (!FileState.STATE_PROCESSING.equals(fileRecord.getFileState())) {
                fileGrid.removeData(fileRecord);
                fireEvent(new FileRemoveEvent(FileUploader.this, fileRecord.getFileName()));
            }
        }
    }

    /**
     * Invoke to stop the upload process.
     */
    private void stopButtonOnClick() {
        // 1. stop current uploading file
        if (uploadingFileRecord != null) {
            uploadingFileRecord.setFileState(FileState.STATE_FAILED);
            uploadingFileRecord.setNote(messageDictionary.get(NOTE_CANCELED));
            fileGrid.updateData(uploadingFileRecord);
            uploadingFileRecord = null;
        }

        // 2. clear fileQueue
        if (fileGrid.getTotalRows() > 0) {
            // update status of file record
            for (FileRecord fileRecord : fileGrid.getSelection()) {
                if (FileState.STATE_PROCESSING.equals(fileRecord.getFileState())) {
                    fileRecord.setFileState(FileState.STATE_QUEUE);
                    fileRecord.setNote(messageDictionary.get(NOTE_QUEUED));
                    fileGrid.updateData(fileRecord);
                }
            }
        }

        // 3. remove formFileUpload
        if (formFileUpload != null) {
            if (formFileUpload.getWidget() != null) {
                formFileUpload.remove(formFileUpload.getWidget());
            }
            formFileUpload.removeFromParent();
            formFileUpload = null;
        }

        resetFileButton.enable();
        uploadFileButton.enable();

        fireEvent(new UploadStopEvent(this));
    }

    /* - File upload handling ----------------------------------------------- */
    /**
     * Try to submit next file in the waiting list.
     */
    private void processNextFile() {
        uploadingFileRecord = findNextFileRecordForUploading();

        if (uploadingFileRecord == null) {
            // remove the iframe for uploading files
            if (formFileUpload != null) {
                formFileUpload.removeFromParent();
                formFileUpload = null;
            }

            uploadFileButton.enable();
            resetFileButton.enable();

            fireEvent(new UploadCompletedEvent(this));
            return;
        }

        // remove the old file input
        if (formFileUpload.getWidget() != null) {
            formFileUpload.remove(formFileUpload.getWidget());
        }

        // add the next file input
        formFileUpload.add(uploadingFileRecord.getFileUploadInput());

        // update its record
        uploadingFileRecord.setFileState(FileState.STATE_PROCESSING);
        fileGrid.updateData(uploadingFileRecord);

        // submit file
        formFileUpload.submit();
    }

    private FileRecord findNextFileRecordForUploading() {
        if (fileGrid.getTotalRows() == 0) {
            return null;
        }
        for (FileRecord fileRecord : fileGrid.getFileRecords()) {
            if (FileState.STATE_QUEUE.equals(fileRecord.getFileState())) {
                return fileRecord;
            }
        }
        return null;
    }

    private void createUploadingForm() {
        formFileUpload = new FileUploadingForm(url);

        Canvas canvas = new WidgetCanvas(formFileUpload);
        canvas.hide();
        addMember(canvas);

        formFileUpload.addSubmitHandler(new SubmitHandler() {
            public void onSubmit(SubmitEvent event) {
                if (uploadingFileRecord != null) {
                    fireEvent(new FileUploadStartEvent(FileUploader.this, uploadingFileRecord.getFileName()));
                }
            }
        });
        formFileUpload.addSubmitCompleteHandler(new SubmitCompleteHandler() {
            public void onSubmitComplete(SubmitCompleteEvent event) {
                GWT.log("Result: " + event.getResults(), null);
                try {
                    JSONValue jsonValue = JSONParser.parseStrict(getJSONFromFormResult(event.getResults()));

                    parseResult(jsonValue);
                } catch (Exception e) {
                    GWT.log("Failed to get response.", e);
                    onFailure(messageDictionary.get(MSG_SERVER_SIDE_ERROR), null);
                }

                // process next one
                processNextFile();
            }
        });
    }

    /**
     * Strip off "pre" tag.
     */
    private String getJSONFromFormResult(String htmlResults) {
        String results = htmlResults.trim();
        if (htmlResults != null && !htmlResults.startsWith("{")) {
            results = results.substring(results.indexOf("{"), results.lastIndexOf("}") + 1);
            return results;
        } else {
            return results;
        }
    }

    private void parseResult(JSONValue jsonValue) {
        JSONObject jsonObject = jsonValue.isObject();

        if (jsonObject != null) {
            String testData = "";
            Set<String> keys = jsonObject.keySet();
            for (String key : keys) {
                testData += key + ":" + jsonObject.get(key) + ";";
            }
            GWT.log(testData, null);

            String result = String.valueOf(jsonObject.get(JSON_KEY_SUCCESS));
            if ("true".equals(result)) {
                onSuccess(messageDictionary.get(MSG_SUCCESS), jsonValue);
            } else {
                onFailure(String.valueOf(jsonObject.get(JSON_KEY_REASON)), jsonValue);
            }
        } else {
            onFailure(messageDictionary.get(MSG_SERVER_SIDE_ERROR), null);
        }
    }

    private void onSuccess(String message, JSONValue jsonValue) {
        // update record of the current uploading file
        if (uploadingFileRecord != null) {
            uploadingFileRecord.setFileState(FileState.STATE_FINISHED);
            uploadingFileRecord.setNote(message);
            fileGrid.updateData(uploadingFileRecord);
        }

        if (uploadingFileRecord != null) {
            fireEvent(new FileUploadSuccessEvent(FileUploader.this, uploadingFileRecord.getFileName(), jsonValue));
        }
    }

    private void onFailure(String message, JSONValue jsonValue) {
        // find the reason
        String reason;
        if ("null".equals(message)) {
            reason = messageDictionary.get(MSG_COULDNT_RETRIEVES_ERVER);
        } else {
            reason = message;
        }
        // update record of the current uploading file
        if (uploadingFileRecord != null) {
            uploadingFileRecord.setFileState(FileState.STATE_FAILED);
            uploadingFileRecord.setNote(reason);
            fileGrid.updateData(uploadingFileRecord);
        }

        if (uploadingFileRecord != null) {
            fireEvent(new FileUploadFailedEvent(FileUploader.this, uploadingFileRecord.getFileName(), jsonValue));
        }
    }

    /* - Other -------------------------------------------------------------- */
    /**
     * @return an unique ID for Record's fineId
     */
    private String generateID() {
        NEXT_ID++;
        return "" + (NEXT_ID - 1);
    }
}
